单页应用与多页应用

传统的 web 项目中,多数是多页应用,即每次页面跳转的时候,后台服务器都会给返回一个新的 html和与之相关的 cssjs 资源,例如熟悉的 JSP ,这种类型的网站也就是多页网站,也叫做多页应用。这种应用最明显的缺点就是页面切换的响应速度慢、静态资源的重复加载等。

而单页应用(SPA)在页面跳转的时候,并不需要请求新的 htmlcssjs 等资源,这样就节省了很多 http 请求,加快了页面切换的速度。但也带来了一个明显的缺点就是第一次加载应用的时候就要获取到所有的页面,导致首屏加载时间较长。幸运的是,服务端渲染(SSR)技术能够很好的解决这个问题。

综合来看,对于中小规模的应用,在如今强调前后端分离的时代,单页应用是很好的选择。接下来的分享是我在利用 vuevue-routervuex 开发单页应用中遇到的一些问题以及对应的解决方法。

单页应用的访问权限

单页应用不需要为每个页面都新建一个 html 文件和若干个 cssjs文件,页面切换不需要后端的控制,看起来貌似对前端开发人员很友好,但实际情况是,对于大多数单页应用而言,前端开发人员需要考虑的东西更多了。主要在于页面访问权限的控制,例如在哈希模式下,用户可以手动改变url的哈希值来访问不同的路由,但在实际应用中,对于用户的这种操作是需要进行判断控制的,例如,某些路由需要登录之后才有权限访问、某些路由对应的页面需要特殊的权限才能访问等。

我将单页应用中的页面类型分为以下两种:

  • 登录页面
  • 需要登录之后才能访问的页面

登录页面的权限控制

登录页面的特殊之处在于,登录页面只应该在用户需要进行登录操作的时候才对其可见,也就是说,若用户的登录状态没有过期,则用户不能访问登录页面的路由。

需要登录之后才能访问的页面的权限控制

vue 应用中,通常会在 mounted 方法中发送 ajax 请求获取数据。但在单页应用中,用户可以改变 url 中的哈希值来访问不同的路由,但对于某些页面,例如个人中心等,需要用户登录之后才能获取到有效的数据进行展示。虽然有一种简单的处理方法:允许用户访问这个路由的页面,但 ajax 获取不到有效数据,但显然不是最好的解决办法。比较好的做法是,通过 vue-router生命周期函数判断进入这些路由前,用户是否已经登录,没有登录则重定向到登录页面的路由。

router.beforeEach((to, from, next) => {
    if (to.path == '/login') { // 访问登录页面的路由
        if (login) { // 登录状态有效,重定向到首页
            next('/');
        } else { // 需要登录
            next();
        }
    } else { // 访问其他的路由
        if (login) { // 登录状态有效,允许访问
            next();
        } else { // 需要登录,重定向到登录页面
            next('/login');
        }
    }
})

登录状态

经过上面的分析,问题最终都指向了 前端如何判断用户的登录状态

通常情况下,前端只负责发送请求,根据响应显示页面的不同内容,而后端会通过 sessionId 或者 token 来判断用户的登录状态是否过期。那么前端有哪些办法(哪些地方) 记录(保存)用户的登录状态是否有效这个数据呢?

客户端存储

sessionStoragelocalStoragecookie 是前端常用的客户端存储方法。

cookie

作为让后端有记忆能力的解决办法,通常会携带一些后端判断请求是来着于谁的信息,如 sessionId / token,如前面所说,这些信息还是由后端判断是否过期,一般不会在 cookie 中直接存储登录状态是否有效。

sessionStorage

存储在浏览器中,可以在用户登录成功之后利用 setItem 保存一个属性,在路由的生命周期函数 beforeEach 中通过 getItem 判断是否已经登录。实现简单,但存在的问题较多:

  • sessionStorage 本身的过期时间决定了用户的登录状态与 sessionStorage 的有效期同步。例如关闭浏览器之后都得重新登录,而不管后端是否设置了 cookieexpire 属性。

    • 后端的登录状态过期了,但 sessionStorage 还有效,导致用户本能访问页面,只是没有数据。用户只能通过一些操作让 sessionStorage 失效(关闭浏览器窗口重新进)
    • 后端的登录状态有效,但 sessionStorage 没有了,导致用户需要重新登录。
  • sessionStorage 是可读可写的,在浏览器的调试工具中可以轻松执行 clear 等方法,也能轻松读取到 setItem 的值,低安全性也决定了 sessionStorage 不能存储重要信息。

localStorage

除了过期时间与 sessionStorage 不一样,其他的功能都类似。

应用存储

在应用中存储,通常就是在 vuex 中记录用户是否登录。在路由的生命周期函数 beforeEach 中读取这个数据来判断用户的登录状态是否有效。那么问题来了:

  • 登录成功之后更新这个数据没问题,还需要什么时候更新这个数据呢?不更新就一直处于登录有效的状态
  • 直接刷新页面之后 vuex 清空了,若后端的登录状态是有效的,也要重新登录吗?

我的解决办法是:通过发送一个请求判断登录状态是否有效。对于获取数据的请求,若返回值中的信息代表需要重新登录了,则清空 vuex 并重定向到登录页面

来看看利用 vuex 记录登录状态时会遇到的所有情况以及处理办法:

router.beforeEach(async (to, from, next) => {
    if (to.path == '/login') { // 访问登录页面的路由
        if (store.login) {
            // 登录状态有效,重定向到首页
            // 这种情况通常是用户手动修改了 url 中的 hash
            next('/');
        } else {
            // 发送 ajax 判断登录状态是否有效
            const login = await ajax();
            if (login) {
                // 修改 store,重定向到首页
                store.commit('login', true);
                next('/');
            } else {
                next();    
            }
        }
    } else { // 访问其他的路由
        if (store.login) {
            // 登录状态有效,允许访问
            next();
        } else {
            // 还没有经过登录页面就访问其他路由,这种情况通常也是用户手动修改了 url 中的 hash
            // 发送 ajax 判断登录状态是否有效
            const login = await ajax();
            if (login) {
                // 修改 store,允许访问
                store.commit('login', true);
                next();
            } else {
                next('/login');    
            }
        }
    }
})

另外,对于每一个获取数据的请求,若后端的返回值中代表用户的登录状态已经过期,则改变 vuex 中的登录状态并重定向到登录页:

store.commit('login', false);
location.href = '/#/login';
location.reload();

李逍
69 声望6 粉丝